/**
 * Copyright 2012 Alfredo "Rainbowbreeze" Morresi
 * 
 * This file is part of Eureka! project.
 * 
 * Eureka! is free software: you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 * 
 * Eureka! is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along
 * with Eureka!. If not, see http://www.gnu.org/licenses/.
 */
package it.rainbowbreeze.eureka.arduino;

import it.rainbowbreeze.libs.common.IRainbowLogFacility;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.InvalidParameterException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;

import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.ParcelUuid;
import android.text.TextUtils;

/**
 * Helpful posts:
 * http://developer.android.com/reference/android/bluetooth/BluetoothDevice.html
 * http://developer.android.com/resources/samples/BluetoothChat/src/com/example/android/BluetoothChat/BluetoothChatService.html
 * http://stackoverflow.com/questions/2660968/how-to-prevent-android-bluetooth-rfcomm-connection-from-dying-immediately-after
 * search for 00001101-0000-1000-8000-00805f9b34fb
 * http://code.google.com/p/carbot/source/browse/trunk/src/net/cardroid/?r=8
 * http://code.google.com/p/backport-android-bluetooth
 * 
 * @author Alfredo "Rainbowbreeze" Morresi
 *
 */
public class BluetoothManager {
    // ------------------------------------------ Private Fields
    private static final String LOG_HASH = BluetoothManager.class.getSimpleName();
    
    private final IRainbowLogFacility mLogFacility;
    private ServiceDiscoveryListener mServiceDiscoveryListener;
    private TimerTask mDiscoveryTimerTask;

    /** enable/disable bluetooth status */
    private BroadcastReceiver mBluetoothStatusReceiver;
    /** new device is picked from the bluetooth device list */
    private BroadcastReceiver mBluetoothDeviceSelected;
    /** pair request */
    private BroadcastReceiver mBluetoothPairReceiver;
    /** Service discovery on remote device */
    private BroadcastReceiver mServiceDiscoveryReceiver;

    private final BluetoothAdapter mBluetoothAdapter;
    private BluetoothDevice mDevice;

    private BluetoothStatusListener mStatusListener;
    private BluetoothDevicePickerListener mDevicePickerListener;


    // -------------------------------------------- Constructors
    public BluetoothManager(IRainbowLogFacility logFacility) {
        mLogFacility = logFacility;
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    }

    // --------------------------------------- Public Properties
    
    public final class ServiceClasses {
        public static final String ServiceDiscoveryServerServiceClassID = "00001000-0000-1000-8000-00805F9B34FB";
        public static final String BrowseGroupDescriptorServiceClassID = "00001001-0000-1000-8000-00805F9B34FB";
        public static final String PublicBrowseGroupServiceClass = "00001002-0000-1000-8000-00805F9B34FB";
        public static final String SerialPortServiceClass = "00001101-0000-1000-8000-00805F9B34FB";
        public static final String LANAccessUsingPPPServiceClass = "00001102-0000-1000-8000-00805F9B34FB";
        public static final String DialupNetworkingServiceClass = "00001103-0000-1000-8000-00805F9B34FB";
        public static final String IrMCSyncServiceClass = "00001104-0000-1000-8000-00805F9B34FB";
        public static final String OBEXObjectPushServiceClass = "00001105-0000-1000-8000-00805F9B34FB";
        public static final String OBEXFileTransferServiceClass = "00001106-0000-1000-8000-00805F9B34FB";
        public static final String IrMCSyncCommandServiceClass = "00001107-0000-1000-8000-00805F9B34FB";
        public static final String HeadsetServiceClass = "00001108-0000-1000-8000-00805F9B34FB";
        public static final String CordlessTelephonyServiceClass = "00001109-0000-1000-8000-00805F9B34FB";
        public static final String AudioSourceServiceClass = "0000110A-0000-1000-8000-00805F9B34FB";
        public static final String AudioSinkServiceClass = "0000110B-0000-1000-8000-00805F9B34FB";
        public static final String AVRemoteControlTargetServiceClass = "0000110C-0000-1000-8000-00805F9B34FB";
        public static final String AdvancedAudioDistributionServiceClass = "0000110D-0000-1000-8000-00805F9B34FB";
        public static final String AVRemoteControlServiceClass = "0000110E-0000-1000-8000-00805F9B34FB";
        public static final String VideoConferencingServiceClass = "0000110F-0000-1000-8000-00805F9B34FB";
        public static final String IntercomServiceClass = "00001110-0000-1000-8000-00805F9B34FB";
        public static final String FaxServiceClass = "00001111-0000-1000-8000-00805F9B34FB";
        public static final String HeadsetAudioGatewayServiceClass = "00001112-0000-1000-8000-00805F9B34FB";
        public static final String WAPServiceClass = "00001113-0000-1000-8000-00805F9B34FB";
        public static final String WAPClientServiceClass = "00001114-0000-1000-8000-00805F9B34FB";
        public static final String PANUServiceClass = "00001115-0000-1000-8000-00805F9B34FB";
        public static final String NAPServiceClass = "00001116-0000-1000-8000-00805F9B34FB";
        public static final String GNServiceClass = "00001117-0000-1000-8000-00805F9B34FB";
        public static final String DirectPrintingServiceClass = "00001118-0000-1000-8000-00805F9B34FB";
        public static final String ReferencePrintingServiceClass = "00001119-0000-1000-8000-00805F9B34FB";
        public static final String ImagingServiceClass = "0000111A-0000-1000-8000-00805F9B34FB";
        public static final String ImagingResponderServiceClass = "0000111B-0000-1000-8000-00805F9B34FB";
        public static final String ImagingAutomaticArchiveServiceClass = "0000111C-0000-1000-8000-00805F9B34FB";
        public static final String ImagingReferenceObjectsServiceClass = "0000111D-0000-1000-8000-00805F9B34FB";
        public static final String HandsfreeServiceClass = "0000111E-0000-1000-8000-00805F9B34FB";
        public static final String HandsfreeAudioGatewayServiceClass = "0000111F-0000-1000-8000-00805F9B34FB";
        public static final String DirectPrintingReferenceObjectsServiceClass = "00001120-0000-1000-8000-00805F9B34FB";
        public static final String ReflectedUIServiceClass = "00001121-0000-1000-8000-00805F9B34FB";
        public static final String BasicPringingServiceClass = "00001122-0000-1000-8000-00805F9B34FB";
        public static final String PrintingStatusServiceClass = "00001123-0000-1000-8000-00805F9B34FB";
        public static final String HumanInterfaceDeviceServiceClass = "00001124-0000-1000-8000-00805F9B34FB";
        public static final String HardcopyCableReplacementServiceClass = "00001125-0000-1000-8000-00805F9B34FB";
        public static final String HCRPrintServiceClass = "00001126-0000-1000-8000-00805F9B34FB";
        public static final String HCRScanServiceClass = "00001127-0000-1000-8000-00805F9B34FB";
        public static final String CommonISDNAccessServiceClass = "00001128-0000-1000-8000-00805F9B34FB";
        public static final String VideoConferencingGWServiceClass = "00001129-0000-1000-8000-00805F9B34FB";
        public static final String UDIMTServiceClass = "0000112A-0000-1000-8000-00805F9B34FB";
        public static final String UDITAServiceClass = "0000112B-0000-1000-8000-00805F9B34FB";
        public static final String AudioVideoServiceClass = "0000112C-0000-1000-8000-00805F9B34FB";
        public static final String SIMAccessServiceClass = "0000112D-0000-1000-8000-00805F9B34FB";
        public static final String PnPInformationServiceClass = "00001200-0000-1000-8000-00805F9B34FB";
        public static final String GenericNetworkingServiceClass = "00001201-0000-1000-8000-00805F9B34FB";
        public static final String GenericFileTransferServiceClass = "00001202-0000-1000-8000-00805F9B34FB";
        public static final String GenericAudioServiceClass = "00001203-0000-1000-8000-00805F9B34FB";
        public static final String GenericTelephonyServiceClass = "00001204-0000-1000-8000-00805F9B34FB";    
    }

    // ------------------------------------------ Public Classes

    public interface ServiceDiscoveryListener {
        public void serviceDiscoveryStarted();
        public void serviceDiscoveryFinished(UUID[] uuids);
        public void serviceDiscoveryFailed();
    }
    
    public interface BluetoothStatusListener {
        public void bluetoothEnabled();
        public void bluetoothDisabled();
    }

    public interface BluetoothDevicePickerListener {
        public void devicePicked(BluetoothDevice device);
    }

    // ------------------------------------------ Public Methods
    
    /**
     * Checks if bluetooth is available on the local device
     * @return
     */
    public boolean isBluetoothAvailable() {
        return mBluetoothAdapter != null;
    }
    
    /**
     * Call during onDestroy method
     * @param context 
     */
    public void unregisterListeners(Context context) {
        if (null != mBluetoothStatusReceiver) {
            mLogFacility.v(LOG_HASH, "Deregistering bluetooth status receiver");
            context.unregisterReceiver(mBluetoothStatusReceiver);
            mBluetoothStatusReceiver = null;
        }
        
        if (null != mBluetoothPairReceiver) {
            mLogFacility.v(LOG_HASH, "Deregistering bluetooth pair receiver");
            context.unregisterReceiver(mBluetoothPairReceiver);
            mBluetoothPairReceiver = null;
        }
    }
    
    /**
     * Tasks to execute during activity onCreate, like registering listeners etc
     * @param context
     */
    public void onCreateTasks(
            Context context,
            BluetoothStatusListener bluetoothStatusListener) {

        if (!isBluetoothAvailable()) {
            mLogFacility.v(LOG_HASH, "Bluetooth is not available");
            return;
        }
        
        IntentFilter filter;
        
        //bluetooth enable / disable
        mStatusListener = bluetoothStatusListener;
        mBluetoothStatusReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                mLogFacility.v(LOG_HASH, "Receiving ACTION_STATE_CHANGED");
                //get new state
                int newState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0);
                mLogFacility.v(LOG_HASH, "EXTRA_STATE value: " + newState);
                switch (newState) {
                case BluetoothAdapter.STATE_ON:
                    if (null != mStatusListener) {
                        mStatusListener.bluetoothEnabled();
                    }
                    break;
                case BluetoothAdapter.STATE_OFF:
                    if (null != mStatusListener) {
                        mStatusListener.bluetoothDisabled();
                    }
                    break;
                default:
                    //TODO Manager missing states
                    // BluetoothAdapter.STATE_TURNING_ON
                    // BluetoothAdapter.STATE_TURNING_OFF
                    break;
                }
            }
        };
        mLogFacility.v(LOG_HASH, "Registering bluetooth status receiver");
        //level 11 API
        // filter = new IntentFilter(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED);
        //level 5 API
        filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
        context.registerReceiver(mBluetoothStatusReceiver, filter);
    }
    
    
    public void onDestroyTasks(Context context) {
        //just in case
        if (null != mBluetoothAdapter) {
            mBluetoothAdapter.cancelDiscovery();
        }
        
        if (null != mServiceDiscoveryReceiver) {
            context.unregisterReceiver(mServiceDiscoveryReceiver);
            mServiceDiscoveryListener = null;
        }
        //TODO unregister all the listener and the receiver
    }
    
    /**
     * 
     * @param newValue
     */
    public void setBluetoothStatusListener(BluetoothStatusListener newValue) {
        mStatusListener = newValue;
    }
    
    public void onResumeTasks(Context context) {
        //unregisters the listener if the user press back on bluetooth device selection system activity
        if (null != mBluetoothDeviceSelected) {
            try {
                mLogFacility.v(LOG_HASH, "It seems that the user doesn't select any bluetooth devices");
                context.unregisterReceiver(mBluetoothDeviceSelected);
            } catch (IllegalArgumentException e) {
                mLogFacility.e(LOG_HASH, "Device selected listener was not registered", e);
            }
        }
    }
    
    public void enableBluetooth(Activity activity) {
        Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        activity.startActivity(intent);
        //also startActivityForResult can be used
    }
    
    
    /**
     * Returns bluetooth activation status
     * @return
     */
    public boolean isBluetoothEnabled() {
        return mBluetoothAdapter.isEnabled();
    }
    
    
    /**
     * 
     * @param deviceAddress
     * @return
     */
    public BluetoothDevice getRemoteDevice(String deviceAddress) {
        return mBluetoothAdapter.getRemoteDevice(deviceAddress);
        
    }
    
    
    /**
     * Returns list of UUIDs from a remote bluetooth device.
     * 
     * @param context
     * @param device
     * @param serviceDiscoveryListener
     */
    public void discoverDeviceServices(
            final Context context,
            BluetoothDevice device,
            ServiceDiscoveryListener listener) {
        
        mServiceDiscoveryListener = listener;
        mDevice = device;
        
        mServiceDiscoveryReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                
                if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action)) {
                    //TODO
                    mLogFacility.v(LOG_HASH, "Receiving action ACTION_ACL_CONNECTED");
                    //listener notification is already managed in the following code

                } else if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(action)) {
                    mLogFacility.v(LOG_HASH, "Receiving action ACTION_ACL_DISCONNECTED");
                    //stop task, no need to perform a force close
                    mDiscoveryTimerTask.cancel();
                    context.unregisterReceiver(mServiceDiscoveryReceiver);
                    if (null != mServiceDiscoveryListener) {
                        mServiceDiscoveryListener.serviceDiscoveryFinished(getUUIDs(mDevice));
                    }
                    mServiceDiscoveryListener = null;
                    
                } else if ("android.bluetooth.device.action.UUID".equals(action)) {
                    mLogFacility.v(LOG_HASH, "Receiving action ACTION_UUID");
                }
            }
        };
        
        IntentFilter filter = new IntentFilter();
        filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
        filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
        filter.addAction("android.bluetooth.device.action.UUID");
        context.registerReceiver(mServiceDiscoveryReceiver, filter);
        
        boolean success = false;
        try {
            success = (Boolean) BluetoothDevice.class.getMethod("fetchUuidsWithSdp", null)
                    .invoke(device, null);
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        if (success) {
            if (null != mServiceDiscoveryListener) {
                mServiceDiscoveryListener.serviceDiscoveryStarted();
            }
            //this timer perform a force close of discovery service, needed for some
            // bluetooth stack buggy implementation
            Timer mTimer = new Timer();
            mDiscoveryTimerTask = new TimerTask() {
                @Override
                public void run() {
                    context.unregisterReceiver(mServiceDiscoveryReceiver);
                    if (null != mServiceDiscoveryListener) {
                        mServiceDiscoveryListener.serviceDiscoveryFinished(getUUIDs(mDevice));
                    }
                    mServiceDiscoveryListener = null;
                }
            };
            mTimer.schedule(mDiscoveryTimerTask, 6000);
            
        } else {
            if (null != mServiceDiscoveryListener)
                mServiceDiscoveryListener.serviceDiscoveryFailed();
        }
    }
    
    /**
     * Configures the environment to pick a bluetooth device from default system activity
     * @param activity
     */
    public void startBluetoothDevicePicking(
            boolean handleAuthorizationDuringPicking,
            Activity activity,
            BluetoothDevicePickerListener bluetoothDevicePickerListener) {
        
        mDevicePickerListener = bluetoothDevicePickerListener;
        
        //device selected after a pick action
        mBluetoothDeviceSelected = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                //once device is selected, unregisters the receiver
                mLogFacility.v(LOG_HASH, "Unregister bluetooth DEVICE_SELECTED");
                context.unregisterReceiver(mBluetoothDeviceSelected);
                mBluetoothDeviceSelected = null;
                //do something with the selected device
                if (null != mDevicePickerListener) {
                    mDevicePickerListener.devicePicked(device);
                }
            }
        };
        mLogFacility.v(LOG_HASH, "Registering bluetooth DEVICE_SELECTED");
        IntentFilter filter = new IntentFilter("android.bluetooth.devicepicker.action.DEVICE_SELECTED");
        activity.registerReceiver(mBluetoothDeviceSelected, filter);
        
        //and, finally, launches the system activity
        Intent intent = new Intent("android.bluetooth.devicepicker.action.LAUNCH");
        //false so the authorization code can be manage from this app,
        // and the user don't need to do nothing
        intent.putExtra("android.bluetooth.devicepicker.extra.NEED_AUTH", handleAuthorizationDuringPicking);
        mLogFacility.v(LOG_HASH, "Launchin device picker system activity");
        activity.startActivity(intent);
    }

    /**
     * Pair a device 
     * 
     * @param context
     * @param deviceAddress
     * @param devicePin
     * @return
     */
    public boolean bondSecureDevice(
            Context context,
            String deviceAddress,
            final String devicePin) {
        
        BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(deviceAddress);

        if (BluetoothDevice.BOND_NONE != device.getBondState()) {
            mLogFacility.v(LOG_HASH, "Bonding already in progress with device " + deviceAddress);
            return true;
        }
        
        mLogFacility.v(LOG_HASH, "Bonding device " + deviceAddress);
        
        //creates a receiver to use during pair request
        mBluetoothPairReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                mLogFacility.v(LOG_HASH, "Requested a pair for device " + device.getAddress());
                if ("android.bluetooth.device.action.PAIRING_REQUEST".equals(intent.getAction())) {
                    byte[] pin;
                    try {
                        pin = (byte[]) BluetoothDevice.class.getMethod("convertPinToBytes", String.class)
                                .invoke(BluetoothDevice.class, devicePin);
                        BluetoothDevice.class.getMethod("setPin", byte[].class)
                                .invoke(device, pin);
                        mLogFacility.v(LOG_HASH, "Pair request managed with pin " + "12345");
                    } catch (IllegalArgumentException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    } catch (NoSuchMethodException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
                mLogFacility.v(LOG_HASH, "Unregistering pair receiver");
                context.unregisterReceiver(mBluetoothPairReceiver);
                mBluetoothPairReceiver = null;
            }
        };
        mLogFacility.v(LOG_HASH, "Registering a pair receiver " + deviceAddress);
        IntentFilter filter = new IntentFilter("android.bluetooth.device.action.PAIRING_REQUEST");
        context.registerReceiver(mBluetoothPairReceiver, filter);
        
        boolean result = false;
        //now, with a receiver ready to manage pin request, it's possible
        // to start a bond with selected device
        mLogFacility.v(LOG_HASH, "Bonding with device " + deviceAddress);
        try {
            Method method = BluetoothDevice.class.getDeclaredMethod("createBond", null);
            result = (Boolean) method.invoke(device, null);
            mLogFacility.v(LOG_HASH, "Bond result: " + result);
        } catch (NoSuchMethodException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return result;
    }
    
    
    public BluetoothSocket createsSerialSocket(String deviceAddress) {
        return createsSerialSocket(deviceAddress, 1, 2, 3);
    }
    
    public BluetoothSocket createsSerialSocket(String deviceAddress, int... ports) {
        if (TextUtils.isEmpty(deviceAddress)) {
            throw new InvalidParameterException("Invalid device address");
        }
        
        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(deviceAddress);
        Method method;
        
        try {
            method = device.getClass().getMethod("createRfcommSocket", new Class[] { int.class });
        } catch (NoSuchMethodException e) {
            mLogFacility.e(LOG_HASH, "createRfcommSocket doesn't exist :(", e);
            return null;
        }        
        //standard serial ports
        if (null == ports) ports = new int[] {1, 2, 3};
        for (int port : ports) {
            BluetoothSocket sock = null;
            try {
                sock = (BluetoothSocket) method.invoke(device, port);
            } catch (IllegalArgumentException e) {
                mLogFacility.e(LOG_HASH, "createRfcommSocket IllegalArgumentException", e);
                return null;
            } catch (IllegalAccessException e) {
                mLogFacility.e(LOG_HASH, "createRfcommSocket IllegalAccessException", e);
                return null;
            } catch (InvocationTargetException e) {
                mLogFacility.e(LOG_HASH, "createRfcommSocket InvocationTargetException", e);
                return null;
            }
            
            try {
                sock.connect();
                sock.close();
                mLogFacility.v(LOG_HASH, "Succesfully open RFCOMM connection for device " + deviceAddress + " on port " + port);
                return sock;
            } catch (IOException e) {
                mLogFacility.v(LOG_HASH, "Cannot open RFCOMM connection for device " + deviceAddress + " on port " + port);
            }
        }
        mLogFacility.v(LOG_HASH, "Unable to connect to RFCOMM for device " + deviceAddress);
        return null;
    }
    
    // ----------------------------------------- Private Methods
    /**
     * Extracts returned UUIDs from a remote device
     * @param device
     * @return
     */
    private UUID[] getUUIDs(BluetoothDevice device) {
        try {
            ParcelUuid[] pUuids = (ParcelUuid[]) BluetoothDevice.class.getMethod("getUuids", null)
                    .invoke(device, null);
            return extractUUIDs(pUuids);
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        return null;
    }

    /**
     * 
     * @param parcels
     * @return
     */
    private UUID[] extractUUIDs(ParcelUuid[] parcels) {
        if (null == parcels) {
            return null;
        }
        UUID[] uuids = new UUID[parcels.length];
        for (int i=0; i<parcels.length; i++) {
            uuids[i] = parcels[i].getUuid();
        }
        return uuids;
    }
    

    // ----------------------------------------- Private Classes
}
